<!--
 Copyright 2024 Google LLC

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->

<template>
    <TreeTable
        :value="filteredTreeData"
        v-model:expandedKeys="expandedKeys"
        size="small"
        :scrollable="true"
        :filters="filters"
        :filterMode="filterMode"
        sortField="namespace"
        :sortOrder="1"
        removableSort
        :rowHover="true"
        :indentation="1.3"
        selectionMode="single"
        @node-select="onNodeSelect"
        :pt="{
            row: ({ instance }) => ({
                oncontextmenu: (event) => onRightClick(event, instance)
            })
        }"
    >
        <template #header>
            <IconField>
                <InputIcon class="pi pi-search" />
                <InputText v-model="filters['global']" placeholder="Global search" />
            </IconField>
        </template>

        <!-- Rule column  -->
        <Column
            field="name"
            header="Rule"
            :sortable="true"
            :expander="true"
            filterMatchMode="contains"
            style="width: 38%"
            class="cursor-default"
        >
            <template #filter v-if="props.showColumnFilters">
                <InputText
                    v-model="filters['name']"
                    type="text"
                    placeholder="Filter by rule or nested feature"
                    class="w-full"
                />
            </template>
            <template #body="{ node }">
                <RuleColumn :node="node" />
            </template>
        </Column>

        <!-- Address column (only shown for static flavor)  -->
        <Column
            v-if="props.data.meta.flavor === 'static'"
            field="address"
            header="Address"
            filterMatchMode="contains"
            style="width: 8.5%"
            class="cursor-default"
        >
            <template #filter v-if="props.showColumnFilters">
                <InputText
                    v-model="filters['address']"
                    type="text"
                    :placeholder="`Filter by ${props.data.meta.flavor === 'dynamic' ? 'process' : 'address'}`"
                    class="w-full"
                />
            </template>
            <template #body="{ node }">
                <span class="font-monospace text-sm">{{ node.data.address }}</span>
            </template>
        </Column>

        <!-- Namespace column -->
        <Column
            field="namespace"
            header="Namespace"
            sortable
            filterMatchMode="contains"
            style="width: 16%"
            class="cursor-default"
        >
            <template #filter v-if="props.showColumnFilters">
                <InputText v-model="filters['namespace']" type="text" placeholder="Filter by namespace" />
            </template>
        </Column>

        <!-- Technique column -->
        <Column
            field="attack"
            header="ATT&CK Technique"
            sortable
            :sortField="(node) => node?.attack[0]?.technique"
            filterField="attack.0.parts"
            filterMatchMode="contains"
            style="width: 15%"
        >
            <template #filter v-if="props.showColumnFilters">
                <InputText
                    v-model="filters['attack.0.parts']"
                    type="text"
                    placeholder="Filter by technique"
                    class="w-full"
                />
            </template>
            <template #body="{ node }">
                <div class="flex flex-wrap">
                    <div v-for="(attack, index) in node.data.attack" :key="index">
                        <a :href="createATTACKHref(attack)" target="_blank">
                            {{ attack.technique }}
                            <span class="text-500 text-sm font-normal ml-1">({{ attack.id.split(".")[0] }})</span>
                        </a>
                        <div v-if="attack.subtechnique" style="font-size: 0.8em; margin-left: 2em">
                            <a :href="createATTACKHref(attack)" target="_blank">
                                ↳ {{ attack.subtechnique }}
                                <span class="text-500 text-xs font-normal ml-1">({{ attack.id }})</span>
                            </a>
                        </div>
                    </div>
                </div>
            </template>
        </Column>

        <!-- MBC column -->
        <Column
            field="mbc"
            header="Malware Behavior Catalog"
            sortable
            :sortField="(node) => node?.mbc[0]?.parts[0]"
            filterField="mbc.0.parts"
            filterMatchMode="contains"
        >
            <template #filter v-if="props.showColumnFilters">
                <InputText v-model="filters['mbc.0.parts']" type="text" placeholder="Filter by MBC" class="w-full" />
            </template>
            <template #body="{ node }">
                <div class="flex flex-wrap">
                    <div v-for="(mbc, index) in node.data.mbc" :key="index">
                        <a :href="createMBCHref(mbc)" target="_blank">
                            {{ mbc.parts.join("::") }}
                            <span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
                        </a>
                    </div>
                </div>
            </template>
        </Column>
    </TreeTable>

    <!-- Right click context menu -->
    <ContextMenu ref="menu" :model="contextMenuItems">
        <template #item="{ item, props }">
            <a v-ripple v-bind="props.action" :href="item.url" :target="item.target">
                <span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
                <VTIcon v-else-if="item.icon === 'vt-icon'" />
                <span>{{ item.label }}</span>
                <i v-if="item.description" class="pi pi-info-circle text-xs" v-tooltip.right="item.description" />
            </a>
        </template>
    </ContextMenu>

    <!-- Source code dialog -->
    <Dialog v-model:visible="sourceDialogVisible" style="width: 50vw">
        <highlightjs :autodetect="false" language="yaml" :code="currentSource" />
    </Dialog>
</template>

<script setup>
// Used to highlight function calls in dynamic mode
import "highlight.js/styles/stackoverflow-light.css";

import { ref, onMounted, computed } from "vue";
import TreeTable from "primevue/treetable";
import InputText from "primevue/inputtext";
import Dialog from "primevue/dialog";
import Column from "primevue/column";
import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon";
import ContextMenu from "primevue/contextmenu";

import RuleColumn from "@/components/columns/RuleColumn.vue";
import VTIcon from "@/components/misc/VTIcon.vue";

import { parseRules } from "@/utils/rdocParser";
import { createMBCHref, createATTACKHref, createCapaRulesUrl, createVirusTotalUrl } from "@/utils/urlHelpers";

const props = defineProps({
    data: {
        type: Object,
        required: true
    },
    showLibraryRules: {
        type: Boolean,
        default: false
    },
    showColumnFilters: {
        type: Boolean,
        default: false
    }
});

const treeData = ref([]);

// The `filters` ref in the setup section is used by PrimeVue to maintain the overall filter
// state of the table. Each column's filter contributes to this overall state.
const filters = ref({});

const filterMode = ref("lenient");
const sourceDialogVisible = ref(false);
const currentSource = ref("");

// expandedKeys keeps track of the nodes that are expanded
// for example, if a node with key "0" is expanded (and its first child is also expanded), expandedKeys will be { "0": true, "0-0": true }
// if the entire tree is collapsed expandedKeys will be {}
const expandedKeys = ref({});

// selectedNode is used as placeholder for the node that is right-clicked
const menu = ref();
const selectedNode = ref({});
const contextMenuItems = computed(() => [
    {
        label: "Copy rule name",
        icon: "pi pi-copy",
        command: () => {
            navigator.clipboard.writeText(selectedNode.value.data?.name);
        }
    },
    {
        label: "View source",
        icon: "pi pi-eye",
        command: () => {
            showSource(selectedNode.value.data?.source);
        }
    },
    {
        label: "View rule in capa-rules",
        icon: "pi pi-external-link",
        target: "_blank",
        url: createCapaRulesUrl(selectedNode.value)
    },
    {
        label: "Lookup rule in VirusTotal",
        icon: "vt-icon",
        target: "_blank",
        description: "Requires VirusTotal Premium account",
        url: createVirusTotalUrl(selectedNode.value.data?.name)
    }
]);

const onRightClick = (event, instance) => {
    if (instance.node.data.source) {
        // We only enable right-click context menu on rows that have
        // a source field (i.e. rules and `- match` features)
        selectedNode.value = instance.node;

        // show the context menu
        menu.value.show(event);
    }
};

/**
 * Handles the expansion and collapse of nodes
 *
 * @param {Object} node - The selected node
 *
 * @example
 * // Expanding a rule node
 * onNodeSelect({
 *   key: '3',
 *   data: { type: 'rule', name: 'test rule', namespace: 'namespace', ... }
 *   children: [
 *      {
 *          key: '3-0',
 *          data: { type: 'match location', name: 'function @ 0x1000', namespace: null, ... }
 *          children: []
 *      }
 *   ]
 * });
 * // Result: expandedKeys.value = { '3': true, '3-0': true }
 */
const onNodeSelect = (node) => {
    const nodeKey = node.key;
    const nodeType = node.data.type;

    // We only expand rule and match locations, otherwise return
    if (nodeType !== "rule" && nodeType !== "match location") return;

    // If the node is already expanded, collapse it
    if (expandedKeys.value[nodeKey]) {
        delete expandedKeys.value[nodeKey];
        return;
    }

    if (nodeType === "rule") {
        // For rule nodes, clear existing expanded keys and set the clicked rule as expanded
        // and expand the first (child) match by default
        expandedKeys.value = { [nodeKey]: true, [`${nodeKey}-0`]: true };
    } else if (nodeType === "match location") {
        // For match location nodes, we need to keep the parent expanded
        // and toggle the clicked node while collapsing siblings
        const [parentKey] = nodeKey.split("-");
        expandedKeys.value = { [parentKey]: true, [`${nodeKey}`]: true };
    }
};

// Filter out the treeData for showing/hiding lib rules
const filteredTreeData = computed(() => {
    if (props.showLibraryRules) {
        return treeData.value; // Return all data when showLibraryRules is true
    } else {
        // Filter out library rules when showLibraryRules is false
        const filterNode = (node) => {
            if (node.data && node.data.lib) {
                return false;
            }
            if (node.children) {
                node.children = node.children.filter(filterNode);
            }
            return true;
        };
        return treeData.value.filter(filterNode);
    }
});

/**
 * Sets the source code of a node in the dialog.
 *
 * @param {string} source - The source code to be displayed.
 */
const showSource = (source) => {
    currentSource.value = source;
    sourceDialogVisible.value = true;
};

onMounted(() => {
    treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
});
</script>

<style scoped>
/* Disable the toggle button for statement and features */
:deep(
        .p-treetable-tbody
            > tr:not(:is([aria-level="1"], [aria-level="2"]))
            > td
            > div
            > .p-treetable-node-toggle-button
    ) {
    visibility: hidden !important;
    height: 1.3rem;
}

/* Make all matches nodes (i.e. not rule names) slightly smaller,
and tighten up the spacing between the rows  */
:deep(.p-treetable-tbody > tr:not([aria-level="1"]) > td) {
    font-size: 0.95rem;
    padding: 0rem 0.5rem !important;
}

/* Optional: Add a subtle background to root-level rows for better distinction  */
:deep(.p-treetable-tbody > tr[aria-level="1"]) {
    background-color: #f9f9f9;
}
</style>
